package ua.edu.nuos.jeetraining2012.cms.util.google.api.services.calendar;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.DateTime;
import com.google.api.services.calendar.model.*;
import org.slf4j.Logger;
import ua.edu.nuos.jeetraining2012.cms.dto.GoogleCalendarDTO;
import ua.edu.nuos.jeetraining2012.cms.dto.GoogleCalendarEventDTO;
import ua.edu.nuos.jeetraining2012.cms.dto.predefined.google.calendar.StandardGoogleCalendarEventVisibility;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.GoogleAuthorizer;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.predefined.StandardGoogleApiVariables;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.auth.GoogleAuthException;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.calendar.*;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.transport.GoogleTransportException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Class that allow to work with an Google Calendar API service.
 *
 * @author Victor N. Ganichev victor.ganichev@gmail.com
 * @version 1.0 Date: 24.07.13 Time: 14:00
 * @since 1.0
 */
public class GoogleCalendar {

    /** Define user's calendars list. */
    private static CalendarList calendarList;

    /** Define HTTP_TRANSPORT */
    private static HttpTransport HTTP_TRANSPORT;

    /** Define Google calendar service */
    private static com.google.api.services.calendar.Calendar calendarService;

    /** Define Application name */
    final private static String APPLICATION_NAME = StandardGoogleApiVariables.APPLICATION_NAME;

    /** Define JSON_FACTORY. */
    final private static JsonFactory JSON_FACTORY = new JacksonFactory();

    /** Define logger */
    final private static Logger logger = org.slf4j.LoggerFactory.getLogger(GoogleCalendar.class);

    public GoogleCalendar() {}

    /**
     * Initiate connection to Calendar service API.
     * Init HTTP_TRANSPORT;
     * Do authorization on service.
     * Create calendarService instance.
     *
     * @throws GoogleTransportException             If can't establish connection with service.
     * @throws GoogleAuthException                  If can't authorize on service.
     */
    private static void initConnection() throws GoogleTransportException, GoogleAuthException {
        logger.debug("Setting up HTTP_TRANSPORT");

        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
            logger.info("calendarEvent() - HTTP_TRANSPORT init successful.");
        } catch (Exception e) {
            logger.error("calendarEvent() - Can't setup HTTP_TRANSPORT. Got Exception with error: {}" +
                    "\nTrace:\n{}", e.getMessage(), e.getStackTrace());
            logger.debug("calendarEvent() - finished with GoogleTransportException.");
            throw new GoogleTransportException("Can't configure Http Transport. Error: " + e.getMessage());
        }

        logger.debug("Trying to authorize...");
        Credential credential = null;
        try {
            credential = GoogleAuthorizer.getInstance()
                    //.setServiceScopes(Collections.singleton(CalendarScopes.CALENDAR))     // Define just Calendar scope.
                    .authorizeEndUser();
            logger.info("calendarEvent() - user success authorized with credentials {}", credential);
        } catch (IOException e) {
            logger.error("calendarEvent() - user can't authorize. Error: {}, \nTrace:\n{}",
                    e.getMessage(), e.getStackTrace());
            logger.debug("calendarEvent() - finished with GoogleAuthException.");
            throw new GoogleAuthException("Can't authorize. Error: " + e.getMessage());
        }

        calendarService = new com.google.api.services.calendar.Calendar
                .Builder(HTTP_TRANSPORT, JSON_FACTORY, null)
                .setHttpRequestInitializer(credential)
                .setApplicationName(APPLICATION_NAME)
                .build();

        logger.debug("Initiate calendar: {}", calendarService);
    }


    /**
     * Define service class for Google Calendar.
     * Main goals is separate actions with calendar from actions with calendar's events.
     * Available actions are:
     * <ul>
     *     <li>create &mdash; will create a new calendar. Useful for just created courses.</li>
     *     <li>...</li>
     * </ul>
     */
    final public static class GCalendar {

        /**
         * Create Google calendar with an info, defined in {@link ua.edu.nuos.jeetraining2012.cms.dto.GoogleCalendarDTO}.
         * <p/>
         * For creating action, the only one field is required. {@link ua.edu.nuos.jeetraining2012.cms.dto.GoogleCalendarDTO#calendarName}.
         * All another fields are optional. Be shore, that name is unique among another calendars.
         * Good practice is to use same name as an SVN repository called.
         * <p/>
         * If no TimeZone set in info DTO, then Europe/Kiev will be used. Complete list of available TimeZones
         * see <a href="http://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> (WiKi).
         * <p/>
         * In case of success, instance of just created calendar will be returned.
         *
         * @param calendarInfoDTO                           User defined info about Google calendar.
         * @return                                          Instance of just created calendar. Useful fields is and id.
         * @throws IllegalArgumentException                 If no dto was sent.
         * @throws GoogleTransportException                 If transport can't be initialised.
         * @throws GoogleAuthException                      If on service auth fails.
         * @throws ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.calendar.GoogleCalendarCreatingException          If can't create calendar.
         */
        public static Calendar create(GoogleCalendarDTO calendarInfoDTO) throws IllegalArgumentException
                , GoogleTransportException, GoogleAuthException, GoogleCalendarCreatingException {

            logger.debug("calendarCreate() - started.");

            if (calendarInfoDTO == null || calendarInfoDTO.getCalendarName() == null ||
                    calendarInfoDTO.getCalendarName().isEmpty()) {
                logger.error("calendarCreate() - no calendar info data set. Please verify, that" +
                        "you had defined calendarName before creating.");
                logger.debug("calendarCreate() - finished with IllegalArgumentException");
                throw new IllegalArgumentException("No calendar name was sent. Can't create without it.");
            }

            logger.debug("Creating new calendar.");
            Calendar calendar = new Calendar();
            calendar.setSummary(calendarInfoDTO.getCalendarName());

            if (calendarInfoDTO.getCalendarDescription() != null && !calendarInfoDTO.getCalendarDescription().isEmpty()) {
                calendar.setDescription(calendarInfoDTO.getCalendarDescription());
            }

            if (calendarInfoDTO.getCalendarLocation() != null && !calendarInfoDTO.getCalendarLocation().isEmpty()) {
                calendar.setLocation(calendarInfoDTO.getCalendarLocation());
            }

            if (calendarInfoDTO.getCalendarTimeZone() != null && !calendarInfoDTO.getCalendarTimeZone().isEmpty()) {
                calendar.setTimeZone(calendarInfoDTO.getCalendarTimeZone());
            } else {
                calendar.setTimeZone("Europe/Kiev");
            }

            initConnection();

            if (isCalendarExists(calendarInfoDTO.getCalendarName())) {
                logger.error("calendarCreate() - Can't create calendar. It already exists.");
                logger.debug("calendarCreate() - finished with GoogleCalendarCreatingException.");
                throw new GoogleCalendarCreatingException("Calendar with name " + calendarInfoDTO.getCalendarName() +
                        " is already exists.");
            }

            Calendar result = null;
            try {
                result = calendarService.calendars().insert(calendar).setDisableGZipContent(true).execute();
                logger.info("calendarCreate() - calendar was created.");
            } catch (IOException e) {
                logger.error("calendarCreate() - can't create calendar. Error: {}, \nTrace:\n{}", e.getMessage(),
                        e.getStackTrace());
                logger.debug("calendarCreate() - finished with GoogleCalendarCreatingException.");
                throw new GoogleCalendarCreatingException("Can't create new calendar. Error: " + e.getMessage());
            }

            if (result == null) {
                logger.error("calendarCreate() - Calendar wasn't created.");
                logger.debug("calendarCreate() - finished with GoogleCalendarCreatingException.");
                throw new GoogleCalendarCreatingException("Calendar wasn't created.");
            }

            try {
                setCalendarPermissions(result.getId());
            } catch (GoogleCalendarNoServiceException e) {
                initConnection();
                try {
                    setCalendarPermissions(result.getId());
                } catch (GoogleCalendarException e1) {
                    throw new GoogleCalendarCreatingException("Can't set calendar permissions");
                }
            } catch (GoogleCalendarPermissionException e) {
                throw new GoogleCalendarCreatingException("Can't set calendar permissions");
            }

            logger.debug("calendarCreate() - finished");
            return result;
        }

        /**
         * Check is calendar present in calendar's list.
         * <p/>
         * First of all, check is calendar list populated with values, if not, try
         * to fetch calendars. If list can't be fetched, return <code>false</code>.<br/>
         * Second, check is calendar with <code>calendarName</code> presents in list.
         * In case of success, return <code>true</code>, either <code>false</code>.
         *
         * @param calendarName          Calendar name, that will be checked.
         * @return                      True if calendar was found among the list.
         */
        private static boolean isCalendarExists(String calendarName) {
            logger.debug("isCalendarExists() - started.");

            if (calendarList == null) {
                try {
                    calendarList = fetchCalendarsList();
                } catch (GoogleCalendarException e) {
                    logger.error("isCalendarExists() - Can't fetch calendar list.");
                    logger.warn("isCalendarExists() - false will be returned.");
                    return false;
                }
            }

            if (calendarList == null) {
                logger.info("No calendars were found, return false.");
                return false;
            }

            for (CalendarListEntry entry : calendarList.getItems()) {
                if (entry.getSummary().equals(calendarName))
                    return true;
            }

            logger.debug("isCalendarExists() - finished.");
            return false;
        }

        /**
         * Fetch all user's calendars.
         * <p/>
         * In case of success, return instance of CalendarList object or null.
         * <p/>
         * If no calendar service was initiated, {@link GoogleCalendarNoServiceException} will be
         * thrown.<br/>
         * If calendars list can't be retrieved, {@link GoogleCalendarListException} will be thrown.
         *
         * @return                                      Instance of CalendarList object.
         * @throws GoogleCalendarNoServiceException     If no service was initiated.
         * @throws GoogleCalendarListException          If calendars list can't be fetched.
         */
        private static com.google.api.services.calendar.model.CalendarList fetchCalendarsList()
                throws GoogleCalendarNoServiceException, GoogleCalendarListException {
            logger.debug("getCalendarsList() - started.");

            if (calendarService == null) {
                logger.error("getCalendarsList() - No service initiated.");
                logger.debug("getCalendarsList() - finished with GoogleCalendarException.");
                throw new GoogleCalendarNoServiceException("No service was initiated.");
            }

            logger.debug("Fetching calendar list.");
            CalendarList calendarList = null;
            try {
                calendarList = calendarService.calendarList().list().setDisableGZipContent(true).execute();
                if (calendarList != null)
                    logger.info("List of calendars was successful fetched. Got {} items", calendarList.getItems().size());

            } catch (IOException e) {
                logger.error("getCalendarsList() - can't retrieve calendar's list. Got error: {}, \nTrace:\n{}",
                        e.getMessage(), e.getStackTrace());
                logger.debug("getCalendarsList() - finished with GoogleCalendarListException.");
                throw new GoogleCalendarListException("Calendar's list can't be retrieved. Error: " + e.getMessage());
            }

            logger.debug("getCalendarsList() - finished.");
            return calendarList;
        }

        /**
         * Set permissions to google calendar as shared. I.e. share for everyone.
         * <p/>
         * In case of success, method will return just inserted permissions.
         * If permissions can't be set, {@link GoogleCalendarPermissionException} will be thrown.
         * If calendar service was not initiated, {@link GoogleCalendarNoServiceException} will be thrown.
         *
         * @param calendarId                            Calendar's ID.
         * @return                                      Inserted permissions.
         * @throws GoogleCalendarNoServiceException     If calendar Service was not initiated.
         * @throws GoogleCalendarPermissionException    If permissions can't be inserted.
         */
        private static AclRule setCalendarPermissions(String calendarId)
                throws GoogleCalendarNoServiceException, GoogleCalendarPermissionException {
            logger.debug("insertCalendarPermissions() - started.");

            if (calendarService == null) {
                logger.error("No service is available. Init service first.");
                logger.debug("insertCalendarPermissions() - finished with GoogleCalendarNoServiceException.");
                throw new GoogleCalendarNoServiceException("No service is available.");
            }

            AclRule rule = new AclRule();
            AclRule.Scope scope = new AclRule.Scope();

            scope.setType("default");
            rule.setScope(scope);
            rule.setRole("reader");

            AclRule createdRule = null;
            logger.debug("insertCalendarPermissions() - Trying to share my calendar.");

            try {
                createdRule = calendarService.acl().insert(calendarId, rule).setDisableGZipContent(true).execute();
            } catch (IOException e) {
                logger.error("insertCalendarPermissions() - can't set permissions. Got error: {}, \nTrace:\n{}",
                        e.getMessage(), e.getStackTrace());
                logger.debug("insertCalendarPermissions() - finished with GoogleCalendarPermissionException");
                throw new GoogleCalendarPermissionException("Can't set calendar permission. Error: " + e.getMessage());
            }

            if (createdRule == null) {
                logger.error("insertCalendarPermissions() - Can't set permissions.");
                logger.debug("insertCalendarPermissions() - finished with GoogleCalendarPermissionException");
                throw new GoogleCalendarPermissionException("Can't set permissions on calendar " + calendarId);
            }

            logger.debug("insertCalendarPermissions() - finished.");
            return createdRule;
        }
    }

    /**
     * Service class for Google Calendar Events.
     */
    final public static class GCalendarEvents {

        /**
         * Create Google Calendar Event.
         * <p/>
         * Use this method, when you want to ad en event, such as lecture (When you are adding materials,
         * the dates of materials should be represented by lectures)
         * <p/>
         *
         * @param eventDto
         * @throws IllegalArgumentException
         * @throws GoogleTransportException
         * @throws GoogleAuthException
         */
        public static Event create(GoogleCalendarEventDTO eventDto) throws IllegalArgumentException,
                GoogleTransportException, GoogleAuthException, GoogleCalendarCreatingException {
            logger.debug("create() - started");

            if (eventDto == null || eventDto.getEventTitle() == null || eventDto.getEventTitle().isEmpty()) {
                logger.error("create() - event dto can't be null, event title can't be null or empty");
                logger.debug("create() - finished with IllegalArgumentException");
                throw new IllegalArgumentException("Calendar event can't be null, event title can't be null or empty");
            }

            if (eventDto.getCalendarId() == null || eventDto.getCalendarId().isEmpty()) {
                logger.error("create() - event's calendar ID should be present.");
                logger.debug("create() - finished with IllegalArgumentException.");
                throw new IllegalArgumentException("Calendar ID should be present");
            }

            if (eventDto.getEventStart() == null || eventDto.getEventStart().before(new java.util.Date())) {
                logger.error("create() - event's start date can't be less than current.");
                logger.debug("create() - finished with IllegalArgumentException.");
                throw new IllegalArgumentException("Calendar event start date can't be before current.");
            }

            if (eventDto.getEventEnd() == null || eventDto.getEventEnd().before(new java.util.Date())) {
                logger.error("create() - event's end date can't be less than current.");
                logger.debug("create() - finished with IllegalArgumentException.");
                throw new IllegalArgumentException("Calendar event end date can't be before current");
            }

            initConnection();

            Event event = new Event();
            EventDateTime eventDateTime = new EventDateTime();
            DateTime dateTimeStart = new DateTime(eventDto.getEventStart());
            DateTime dateTimeEnd = new DateTime(eventDto.getEventEnd());

            event.setSummary(eventDto.getEventTitle());
            event.setStart(eventDateTime.setDateTime(dateTimeStart));
            event.setEnd(eventDateTime.setDateTime(dateTimeEnd));

            if (eventDto.getEventDescription() != null && !eventDto.getEventDescription().isEmpty())
                event.setDescription(eventDto.getEventDescription());

            if (eventDto.getEventOrganizer() != null && !eventDto.getEventOrganizer().isEmpty())
                event.setOrganizer(new Event.Organizer().setDisplayName(eventDto.getEventOrganizer()));

            if (eventDto.getEventVisibility() != null && !eventDto.getEventVisibility().isEmpty())
                event.setVisibility(eventDto.getEventVisibility());
            else
                event.setVisibility(StandardGoogleCalendarEventVisibility.EVENT_VIS_PUBLIC);

            if (eventDto.getAttendeesList() != null)
                event.setAttendees(eventDto.getAttendeesList());

            if (eventDto.getEventReminder() != null) {
                Event.Reminders reminders = new Event.Reminders();

                EventReminder eventReminder = new EventReminder();
                eventReminder.setMethod(eventDto.getEventReminder())
                             .setMinutes(eventDto.getEventReminderMinutes());
                List<EventReminder> eventReminderList = new ArrayList<>();
                eventReminderList.add(eventReminder);
                reminders.setUseDefault(false);
                reminders.setOverrides(eventReminderList);

                event.setReminders(reminders);
            }

            if (!eventDto.isGuestToGuestVisibility())
                event.setGuestsCanSeeOtherGuests(eventDto.isGuestToGuestVisibility());

            Event result = null;
            try {
                result = calendarService.events().insert(eventDto.getCalendarId(), event)
                        .setDisableGZipContent(true)        // Do not remove this, before Google fix method implementation.
                        .execute();
                logger.info("create() - event was add.");
            } catch (IOException e) {
                logger.error("create() - can't create Google Calendar Event. Error: {}, \nTrace:\n{}", e.getMessage(),
                        e.getStackTrace());
                logger.debug("create() - finished with GoogleCalendarCreatingException");
                throw new GoogleCalendarCreatingException("Calendar's event can't be created. Error: " + e.getMessage());
            }

            logger.debug("calendarEvent() - finished");
            return result;
        }
    }

}
